﻿using System;
using System.ComponentModel;
using System.Threading;
using System.IO;
using System.Collections.Generic;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Graphics.PackedVector;

using xEngine.Maths;
using xEngine.Utils;
using xEngine.DataTypes;
using xEngine.Debugging;
using xEngine.Rendering.Effects;

namespace xEngine.Actors.Terrain
{
    public class Terrain : Actor
    {        
        #region Member Variables

        private TerrainQuadTree _quadTree;

        private BackgroundWorker _worker;
        private bool _useThreading;
        private bool _threadWaiting;
        private bool _boundsInvalid;

        #endregion

        #region Properties

        public bool UseThreading
        {
            get { return _useThreading; }
            set
            {
                // Changed to true
                if (!_useThreading && (_useThreading = value))
                {
                    _worker.RunWorkerAsync();
                }
                // Changed to false
                else if (_useThreading && !(_useThreading = value))
                {
                    _worker.CancelAsync();
                }
            }
        }
        public int ThreadInterval { get; set; }
        public int MinQuality { get; set; }
        public int MaxQuality { get; set; }
        public int MaxRealQuality { get { return HeightMap != null ? (int)Math.Log((HeightMap.Width - 1) / 2, 2) + 2 : 0; } }
        public float FarDistance { get; set; }
        public float NearDistance { get; set; }

        public GraphicsDevice GraphicsDevice { get; private set; }
        public Texture2D HeightMap { get; private set; }

        public List<TerrainLayer> Layers { get; private set; }
        public Texture2D AlphaMap { get; private set; }

        public TerrainCursor TerrainCursor { get; private set; }

        #endregion

        #region Constructors

        public Terrain(GraphicsDevice device)
        {
            GraphicsDevice = device;

            Collidable = true;

            _worker = new BackgroundWorker();
            _worker.WorkerSupportsCancellation = true;
            _worker.DoWork += new DoWorkEventHandler(WorkerAsync);

            UseThreading = true;
            ThreadInterval = 500;
            MinQuality = 8;
            MaxQuality = 10;
            FarDistance = 2000;
            NearDistance = 500;

            DefaultShader = new FXTerrain(this);

            Layers = new List<TerrainLayer>();

            TerrainCursor = new TerrainCursor();
            TerrainCursor.Scale = Vector2.One * 10;
        }
        public Terrain(GraphicsDevice device, int resolution) : this(device)
        {
            LoadHeightMap(new Texture2D(device, resolution, resolution, false, SurfaceFormat.Color));
        }
        public Terrain(GraphicsDevice device, Texture2D heightMap) : this(device)
        {
            if (heightMap != null)
                LoadHeightMap(heightMap);
        }
        public Terrain(GraphicsDevice device, string heightMap) : this(device, heightMap, HeightMapType.Raw16) { }
        public Terrain(GraphicsDevice device, string heightMap, HeightMapType type) : this(device)
        {
            if (!string.IsNullOrEmpty(heightMap))
                LoadHeightMap(heightMap, type);
        }

        #endregion

        #region Functions

        private void UpdateAsync()
        {
            if (_quadTree != null)
            {
                if (_boundsInvalid)
                {
                    _quadTree.UpdateBounds();
                    BoundingShape = new BoundingShape(_quadTree.RootNode.BoundingBox.Min, _quadTree.RootNode.BoundingBox.Max);
                    _boundsInvalid = false;
                }

                _quadTree.UpdateAsync();
            }
        }
        private void WorkerAsync(object sender, DoWorkEventArgs e)
        {
            while (!_worker.CancellationPending)
            {
                UpdateAsync();
                System.Threading.Thread.Sleep(ThreadInterval);
            }

            _threadWaiting = false;
        }
        
        public float GetHeight(float x, float z)
        {
            float height = -1;

            Vector2 p = new Vector2(x, z);

            TerrainQuadTreeNode containingNode = _quadTree.FindNodeContainingPoint(p);

            if (containingNode != null)
            {
                float fX = x / Transformation.Scale.X;
                float fZ = z / Transformation.Scale.Z;

                if (fX == (int)fX && fZ == (int)fZ)
                    height = _quadTree.Vertices[(int)(fZ * HeightMap.Height) + (int)fX].Position.Y;
                else
                    height = containingNode.FindTriangleContainingPoint(p).InterpolateY(p);
            }

            return height;
        }

        public void LoadHeightMap(string path)
        {
            LoadHeightMap(path, HeightMapType.Raw16);
        }
        public void LoadHeightMap(string path, HeightMapType type)
        {
            Stream s = DataLoader.GetStream(path);
            byte[] bytes;
            int width;
            bool success;

            if (s == null || s.Length == 0)
                return;

            if (type == HeightMapType.Png8)
            {
                try
                {
                	HeightMap = Texture2D.FromStream(GraphicsDevice, s);
                    success = true;
                }
                catch (System.Exception e)
                {
                    LogWriter.WriteLine = e;
                    success = false;
                }
            }
            else
            {
                bytes = new byte[s.Length];
                s.Read(bytes, 0, (int)s.Length);
                width = (int)Math.Sqrt(type == HeightMapType.Raw8 ? s.Length : (s.Length / 2));
                try
                {
                    HeightMap = new Texture2D(GraphicsDevice, width, width, false, type == HeightMapType.Raw8 ? SurfaceFormat.Alpha8 : SurfaceFormat.HalfSingle);
                    HeightMap.SetData<byte>(bytes);
                    success = true;
                }
                catch (Exception e)
                {
                    LogWriter.WriteLine = e;
                    success = false;
                }
            }

            s.Dispose();
            
            if(!success)
                LoadHeightMap(new Texture2D(GraphicsDevice, 257, 257, false, SurfaceFormat.Color));

            PrepareTerrain();
        }
        public void LoadHeightMap(Texture2D heightMap)
        {
            HeightMap = heightMap;
            PrepareTerrain();
        }
        private void PrepareTerrain()
        {
            if ((MinQuality = MaxRealQuality - 2) < 1)
                MinQuality = 1;
            MaxQuality = MaxRealQuality;

            _quadTree = new TerrainQuadTree(this);
            _quadTree.UpdateBounds();
            BoundingShape = new BoundingShape(_quadTree.RootNode.BoundingBox.Min, _quadTree.RootNode.BoundingBox.Max);

            AlphaMap = new Texture2D(GraphicsDevice, HeightMap.Width, HeightMap.Height, false, SurfaceFormat.Color);

            Color[] c = new Color[HeightMap.Width * HeightMap.Height];
            Random r = new Random();

            for (int i = 0; i < c.Length; i++)
                c[i] = Color.FromNonPremultiplied(128, 128, 0, 255);

            AlphaMap.SetData<Color>(c);
        }
        public void SaveHeightMap(string path)
        {
            SaveHeightMap(path, HeightMapType.Raw16);
        }
        public void SaveHeightMap(string path, HeightMapType type)
        {
            if(HeightMap == null)
                return;

            Stream s = File.OpenWrite(path);
            byte[] bytes;
            int offset = 0;

            switch (type)
            {
                case HeightMapType.Png8:
                    if (HeightMap.Format == SurfaceFormat.Color)
                        HeightMap.SaveAsPng(s, HeightMap.Width, HeightMap.Height);
                    else
                    {
                        Texture2D t = new Texture2D(GraphicsDevice, HeightMap.Width, HeightMap.Height, false, SurfaceFormat.Color);
                        bytes = new byte[HeightMap.Width * HeightMap.Height * 4];

                        for (int x = 0; x < HeightMap.Width; x++)
                            for (int z = 0; z < HeightMap.Height; z++)
                            {
                                bytes[offset++] = (byte)(_quadTree.Vertices[x * HeightMap.Height + z].Position.Y * 255.0f);
                                bytes[offset++] = (byte)(_quadTree.Vertices[x * HeightMap.Height + z].Position.Y * 255.0f);
                                bytes[offset++] = (byte)(_quadTree.Vertices[x * HeightMap.Height + z].Position.Y * 255.0f);
                                bytes[offset++] = 255;
                            }

                        t.SetData(bytes);                        
                        t.SaveAsPng(s, t.Width, t.Height);
                    }
                    break;

                case HeightMapType.Raw8:
                    bytes = new byte[HeightMap.Width * HeightMap.Height];

                    for (int x = 0; x < HeightMap.Width; x++)
                        for (int z = 0; z < HeightMap.Height; z++)
                            bytes[offset++] = (byte)(_quadTree.Vertices[x * HeightMap.Height + z].Position.Y * 255.0f);

                    s.Write(bytes, 0, offset);
                    break;

                default:
                    bytes = new byte[HeightMap.Width * HeightMap.Height * 2];

                    for (int x = 0; x < HeightMap.Width; x++)
                        for (int z = 0; z < HeightMap.Height; z++)
                        {
                            ushort height = (ushort)(_quadTree.Vertices[x * HeightMap.Height + z].Position.Y * 65535.0f);
                            bytes[offset++] = (byte)height;
                            bytes[offset++] = (byte)(height >> 8);
                        }

                    s.Write(bytes, 0, offset);
                    break;
            }

            s.Dispose();
        }
        public Texture2D GetHeightMap(HeightMapType type)
        {
            if (HeightMap == null)
                return null;

            Texture2D output;

            switch (type)
            {
                case HeightMapType.Png8:
                case HeightMapType.Raw8:
                    output = new Texture2D(GraphicsDevice, HeightMap.Width, HeightMap.Height, false, SurfaceFormat.Color);
                    Color[] c = new Color[HeightMap.Width * HeightMap.Height];

                    for (int x = 0; x < HeightMap.Width; x++)
                        for (int z = 0; z < HeightMap.Height; z++)
                            c[z * HeightMap.Width + x] = Color.FromNonPremultiplied((int)(_quadTree.Vertices[z * HeightMap.Width + x].Position.Y * 255.0f), 0, 0, 255);

                    output.SetData(c);
                    break;
                
                default:
                    output = new Texture2D(GraphicsDevice, HeightMap.Width, HeightMap.Height, false, SurfaceFormat.HalfSingle);
                    HalfSingle[] h = new HalfSingle[HeightMap.Width * HeightMap.Height];

                    for (int x = 0; x < HeightMap.Width; x++)
                        for (int z = 0; z < HeightMap.Height; z++)
                            h[z * HeightMap.Width + x] = new HalfSingle(_quadTree.Vertices[z * HeightMap.Width + x].Position.Y);

                    output.SetData(h);
                    break;
            }

            return output;
        }

        public void ResetHeightMap()
        {
            if(HeightMap != null)
                LoadHeightMap(new Texture2D(GraphicsDevice, HeightMap.Width, HeightMap.Height, false, SurfaceFormat.Color));
        }

        public override void Update(GameTime time)
        {
            if(!Enabled || _quadTree == null)
                return;

            _quadTree.Update(Camera.DefaultCamera.Transformation.Position);

            if (!UseThreading)
                UpdateAsync();
        }
        public override void Render()
        {
            if (!Visible || _quadTree == null)
                return;

            if (_quadTree.IndicesCount < 3)
                return;

            GraphicsDevice.SetVertexBuffer(_quadTree.VertexBuffer);
            GraphicsDevice.Indices = _quadTree.IndexBuffer;

            int remainingPrimitives = _quadTree.IndicesCount / 3;
            int toRender;
            int offset = 0;
            
            while(remainingPrimitives > 0)
            {
                toRender = Math.Min(remainingPrimitives, 1024 * 1024 - 1);
                GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, _quadTree.Vertices.VertexCount, offset, toRender);
                offset += toRender * 3;
                remainingPrimitives -= toRender;
            }

            GraphicsDevice.Indices = null;
        }

        protected override void OnTransformMatrixUpdate()
        {
            _boundsInvalid = true;
        }
        
        public override void Dispose()
        {
            // Stop update thread if exists
            _threadWaiting = true;
            UseThreading = false;
            while (_threadWaiting) Thread.Sleep(0);

            base.Dispose();
        }

        public override IntersectionResult Collides(Actor actor, bool forceCheck)
        {
            return (((Collidable && actor.Collidable) || forceCheck) && _quadTree != null) ? _quadTree.RootNode.Intersects(actor) : false;
        }
        public override IntersectionResult Collides(Ray ray, bool forceCheck)
        {
            return ((Collidable || forceCheck) && _quadTree != null) ? _quadTree.RootNode.Intersects(ray) : false;
        }

        #endregion
    }

    public class TerrainLayer
    {
        public Texture2D Texture { get; set; }
        public Vector2 Offset { get; set; }
        public Vector2 Scale { get; set; }
        public Vector4 Color { get; set; }

        public TerrainLayer() { }
        public TerrainLayer(Texture2D texture, Vector2 offset, Vector2 scale, Vector4 color)
        {
            Texture = texture;
            Offset = offset;
            Scale = scale;
            Color = color;
        }
    }

    public class TerrainCursor
    {
        public bool Show { get; set; }
        public Texture2D Texture { get; set; }
        public Vector2 Position { get; set; }
        public Vector2 Scale { get; set; }
        public Vector4 Color { get; set; }
    }

    public enum HeightMapType
    {
        Png8,
        Raw8,
        Raw16
    }
}
